package com.ruibo.utils;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.util.StringUtils;

import javax.mail.internet.MimeUtility;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;

public class DownloadUtil {

    private static class Range {
        private static void copy(final InputStream input, final OutputStream output, final long inputSize,
                                 final long start, final long length) throws IOException {
            final byte[] buffer = new byte[20480];
            int read;

            if (inputSize == length) {
                // Write full range.
                while ((read = input.read(buffer)) > 0) {
                    output.write(buffer, 0, read);
                    output.flush();
                }
            } else {
                input.skip(start);
                long toRead = length;

                while ((read = input.read(buffer)) > 0) {
                    if ((toRead -= read) > 0) {
                        output.write(buffer, 0, read);
                        output.flush();
                    } else {
                        output.write(buffer, 0, (int) toRead + read);
                        output.flush();
                        break;
                    }
                }
            }
        }

        public static long sublong(final String value, final int beginIndex, final int endIndex) {
            final String substring = value.substring(beginIndex, endIndex);
            return (substring.length() > 0) ? Long.parseLong(substring) : -1;
        }

        long start;
        long end;

        long length;

        long total;

        /**
         * Construct a byte range.
         *
         * @param start Start of the byte range.
         * @param end   End of the byte range.
         * @param total Total length of the byte source.
         */
        public Range(final long start, final long end, final long total) {
            this.start = start;
            this.end = end;
            this.length = (end - start) + 1;
            this.total = total;
        }
    }

    //    public static void main(final String[] args) {
    //        final String a = "a.JPG";
    //        System.out.println(DownloadUtil.getExtension(a));
    //    }

    private static final Map<String, String> contentTypes = new HashMap<String, String>() {
        private static final long serialVersionUID = 7227528070332985770L;

        {
            this.put("ez", "application/andrew-inset");
            this.put("hqx", "application/mac-binhex40");
            this.put("cpt", "application/mac-compactpro");
            this.put("doc", "application/msword");
            this.put("docx", "application/msword");
            this.put("bin", "application/octet-stream");
            this.put("dms", "application/octet-stream");
            this.put("lha", "application/octet-stream");
            this.put("lzh", "application/octet-stream");
            this.put("exe", "application/octet-stream");
            this.put("class", "application/octet-stream");
            this.put("so", "application/octet-stream");
            this.put("dll", "application/octet-stream");
            this.put("oda", "application/oda");
            this.put("pdf", "application/pdf");
            this.put("ai", "application/postscript");
            this.put("json", "application/json");
            this.put("eps", "application/postscript");
            this.put("ps", "application/postscript");
            this.put("smi", "application/smil");
            this.put("smil", "application/smil");
            this.put("mif", "application/vnd.mif");
            this.put("xls", "application/vnd.ms-excel");
            this.put("xlsx", "application/vnd.ms-excel");
            this.put("ppt", "application/vnd.ms-powerpoint");
            this.put("pptx", "application/vnd.ms-powerpoint");
            this.put("wbxml", "application/vnd.wap.wbxml");
            this.put("wmlc", "application/vnd.wap.wmlc");
            this.put("wmlsc", "application/vnd.wap.wmlscriptc");
            this.put("bcpio", "application/x-bcpio");
            this.put("vcd", "application/x-cdlink");
            this.put("pgn", "application/x-chess-pgn");
            this.put("cpio", "application/x-cpio");
            this.put("csh", "application/x-csh");
            this.put("dcr", "application/x-director");
            this.put("dir", "application/x-director");
            this.put("dxr", "application/x-director");
            this.put("dvi", "application/x-dvi");
            this.put("spl", "application/x-futuresplash");
            this.put("gtar", "application/x-gtar");
            this.put("hdf", "application/x-hdf");
            this.put("js", "application/x-javascript");
            this.put("skp", "application/x-koan");
            this.put("skd", "application/x-koan");
            this.put("skt", "application/x-koan");
            this.put("skm", "application/x-koan");
            this.put("latex", "application/x-latex");
            this.put("nc", "application/x-netcdf");
            this.put("cdf", "application/x-netcdf");
            this.put("sh", "application/x-sh");
            this.put("shar", "application/x-shar");
            this.put("swf", "application/x-shockwave-flash");
            this.put("sit", "application/x-stuffit");
            this.put("sv4cpio", "application/x-sv4cpio");
            this.put("sv4crc", "application/x-sv4crc");
            this.put("tar", "application/x-tar");
            this.put("tcl", "application/x-tcl");
            this.put("tex", "application/x-tex");
            this.put("texinfo", "application/x-texinfo");
            this.put("texi", "application/x-texinfo");
            this.put("t", "application/x-troff");
            this.put("tr", "application/x-troff");
            this.put("roff", "application/x-troff");
            this.put("man", "application/x-troff-man");
            this.put("me", "application/x-troff-me");
            this.put("ms", "application/x-troff-ms");
            this.put("ustar", "application/x-ustar");
            this.put("src", "application/x-wais-source");
            this.put("xhtml", "application/xhtml+xml");
            this.put("xht", "application/xhtml+xml");
            this.put("zip", "application/zip");
            this.put("au", "audio/basic");
            this.put("snd", "audio/basic");
            this.put("mid", "audio/midi");
            this.put("midi", "audio/midi");
            this.put("kar", "audio/midi");
            this.put("mpga", "audio/mpeg");
            this.put("mp2", "audio/mpeg");
            this.put("mp3", "audio/mpeg");
            this.put("mp4", "video/mp4");
            this.put("aif", "audio/x-aiff");
            this.put("aiff", "audio/x-aiff");
            this.put("aifc", "audio/x-aiff");
            this.put("m3u", "audio/x-mpegurl");
            this.put("ram", "audio/x-pn-realaudio");
            this.put("rm", "audio/x-pn-realaudio");
            this.put("rpm", "audio/x-pn-realaudio-plugin");
            this.put("ra", "audio/x-realaudio");
            this.put("wav", "audio/x-wav");
            this.put("pdb", "chemical/x-pdb");
            this.put("xyz", "chemical/x-xyz");
            this.put("bmp", "image/bmp");
            this.put("gif", "image/gif");
            this.put("ief", "image/ief");
            this.put("jpeg", "image/jpeg");
            this.put("jpg", "image/jpeg");
            this.put("jpe", "image/jpeg");
            this.put("png", "image/png");
            this.put("tiff", "image/tiff");
            this.put("tif", "image/tiff");
            this.put("djvu", "image/vnd.djvu");
            this.put("djv", "image/vnd.djvu");
            this.put("wbmp", "image/vnd.wap.wbmp");
            this.put("ras", "image/x-cmu-raster");
            this.put("pnm", "image/x-portable-anymap");
            this.put("pbm", "image/x-portable-bitmap");
            this.put("pgm", "image/x-portable-graymap");
            this.put("ppm", "image/x-portable-pixmap");
            this.put("rgb", "image/x-rgb");
            this.put("xbm", "image/x-xbitmap");
            this.put("xpm", "image/x-xpixmap");
            this.put("xwd", "image/x-xwindowdump");
            this.put("igs", "model/iges");
            this.put("iges", "model/iges");
            this.put("msh", "model/mesh");
            this.put("mesh", "model/mesh");
            this.put("silo", "model/mesh");
            this.put("wrl", "model/vrml");
            this.put("vrml", "model/vrml");
            this.put("css", "text/css");
            this.put("html", "text/html");
            this.put("htm", "text/html");
            this.put("asc", "text/plain");
            this.put("txt", "text/plain");
            this.put("rtx", "text/richtext");
            this.put("rtf", "text/rtf");
            this.put("sgml", "text/sgml");
            this.put("sgm", "text/sgml");
            this.put("tsv", "text/tab-separated-values");
            this.put("wml", "text/vnd.wap.wml");
            this.put("wmls", "text/vnd.wap.wmlscript");
            this.put("etx", "text/x-setext");
            this.put("xsl", "text/xml");
            this.put("xml", "text/xml");
            this.put("mpeg", "video/mpeg");
            this.put("mpg", "video/mpeg");
            this.put("mpe", "video/mpeg");
            this.put("qt", "video/quicktime");
            this.put("mov", "video/quicktime");
            this.put("mxu", "video/vnd.mpegurl");
            this.put("avi", "video/x-msvideo");
            this.put("movie", "video/x-sgi-movie");
            this.put("ice", "x-conference/x-cooltalk");
        }
    };

    public static void download(final HttpServletRequest request,
                                final HttpServletResponse response, final File file,
                                String fileName) throws IOException {
        String contentType = "";
        final String extension = DownloadUtil.getExtension(file.getName());
        if (StringUtils.hasText(extension)) {
            contentType = DownloadUtil.contentTypes.get(extension);
        }

        final Long length = file.length();
        final Long lastModified = file.lastModified();
        final String ETag = file.getName();
        final String ifNoneMatch = request.getHeader("If-None-Match");
        {
            // Validate request headers for caching ---------------------------------------------------
            // If-None-Match header should contain "*" or ETag. If so, then return 304.
            if ((ifNoneMatch != null) && DownloadUtil.matches(ifNoneMatch, ETag)) {
                response.setHeader("ETag", ETag); // Required in 304.
                response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }
        }

        {
            // If-Modified-Since header should be greater than LastModified. If so, then return 304.
            // This header is ignored if any If-None-Match header is specified.
            final long ifModifiedSince = request.getDateHeader("If-Modified-Since");
            if ((ifNoneMatch == null) && (ifModifiedSince != -1) && ((ifModifiedSince + 1000) > lastModified)) {
                response.setHeader("ETag", ETag); // Required in 304.
                response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }
        }

        {
            // Validate request headers for resume ----------------------------------------------------
            // If-Match header should contain "*" or ETag. If not, then return 412.
            final String ifMatch = request.getHeader("If-Match");
            if ((ifMatch != null) && !DownloadUtil.matches(ifMatch, ETag)) {
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return;
            }
        }

        {
            // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
            final long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
            if ((ifUnmodifiedSince != -1) && ((ifUnmodifiedSince + 1000) <= lastModified)) {
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return;
            }
        }

        final Range full = new Range(0, length - 1, length);
        final List<Range> ranges = DownloadUtil.getRanges(ETag, request, response, length, full);
        if (ranges == null) {
            return;
        }

        {
            // Initialize response.
            if (!StringUtils.hasText(fileName)) {
                fileName = file.getName();
            }
            fileName = DownloadUtil.encodeFilename(request, fileName);
            response.setBufferSize(20480);
            response.setContentType(contentType);
            if ((contentType == null)
                    || (!contentType.startsWith("video") && !contentType.startsWith("audio"))) {
                response.setHeader("Content-Disposition", "attachment;" + fileName);
            }
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("ETag", ETag);
            response.setDateHeader("Last-Modified", lastModified);
            response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L);
        }

        DownloadUtil.writeResponse(response, contentType, file, full, ranges);
    }

    public static void downloadTxt(final String filename, final String fileText,
                                   final HttpServletRequest request, final HttpServletResponse response) throws IOException {
        response.setHeader("Content-Type", "application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;fileName="
                + filename + ".txt");
        response.setHeader("Content-Length", String.valueOf(fileText.getBytes().length));
        try (OutputStream output = response.getOutputStream()) {
            output.write(fileText.getBytes("utf-8"));
            output.flush();
        } catch (final Exception e) {
            // 可能连接被重置
        }
    }

    private static String encodeFilename(final HttpServletRequest request, final String filename) {
        String userAgent = request.getHeader("User-Agent");
        if (userAgent == null) {
            userAgent = "";
        }
        try {
            userAgent = userAgent.toLowerCase();
            if (userAgent.indexOf("msie") != -1) {
                // IE浏览器，只能采用URLEncoder编码
                return "filename=\"" + java.net.URLEncoder.encode(filename, "UTF-8") + "\"";
            } else if (userAgent.indexOf("opera") != -1) {
                // Opera浏览器只能采用filename*
                return "filename*=UTF-8''" + filename;
            } else if (userAgent.indexOf("safari") != -1) {
                // Safari浏览器，只能采用ISO编码的中文输出
                return "filename=\"" + new String(filename.getBytes("UTF-8"), "ISO8859-1") + "\"";
            } else if (userAgent.indexOf("applewebkit") != -1) {
                // Chrome浏览器，只能采用MimeUtility编码或ISO编码的中文输出
                final String newFilename = MimeUtility.encodeText(filename, "UTF-8", "B");
                return "filename=\"" + newFilename + "\"";
            } else if (userAgent.indexOf("mozilla") != -1) {
                // FireFox浏览器，可以使用MimeUtility或filename*或ISO编码的中文输出
                return "filename*=UTF-8''" + filename;
            } else {
                return "filename=\"" + java.net.URLEncoder.encode(filename, "UTF-8") + "\"";
            }
        } catch (final UnsupportedEncodingException e) {
            return "filename=\"" + filename + "\"";
        }
    }

    public static String getExtension(final String fileName) {
        if (fileName == null) {
            return null;
        }
        final int index = fileName.lastIndexOf(".");
        if (index != -1) {
            return fileName.toLowerCase().substring(index + 1, fileName.length());
        } else {
            return fileName;
        }
    }

    private static List<Range> getRanges(final String ETag, final HttpServletRequest request,
                                         final HttpServletResponse response,
                                         final Long length,
                                         final Range full) throws IOException {

        // Prepare some variables. The full Range represents the complete file.
        final List<Range> ranges = new ArrayList<>();

        // Validate and process Range and If-Range headers.
        final String range = request.getHeader("Range");
        if (range != null) {
            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return null;
            }

            final String ifRange = request.getHeader("If-Range");
            if ((ifRange != null) && !ifRange.equals(ETag)) {
                try {
                    final long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                    if (ifRangeTime != -1) {
                        ranges.add(full);
                    }
                } catch (final IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte range.
            if (ranges.isEmpty()) {
                for (final String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following examples returns bytes at,
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = Range.sublong(part, 0, part.indexOf("-"));
                    long end = Range.sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if ((end == -1) || (end > (length - 1))) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return null;
                    }

                    // Add range.
                    ranges.add(new Range(start, end, length));
                }
            }
        }
        return ranges;
    }

    private static boolean matches(final String matchHeader, final String toMatch) {
        final String[] matchValues = matchHeader.split("\\s*,\\s*");
        Arrays.sort(matchValues);
        return (Arrays.binarySearch(matchValues, toMatch) > -1)
                || (Arrays.binarySearch(matchValues, "*") > -1);
    }

    private static void writeResponse(final HttpServletResponse response, final String contentType, final File file,
                                      final Range full, final List<Range> ranges) throws FileNotFoundException, IOException {
        // Send requested file (part(s)) to client ------------------------------------------------
        // Prepare streams.
        final Long length = file.length();
        InputStream input = null;
        try (OutputStream output = response.getOutputStream()) {
            input = new BufferedInputStream(new FileInputStream(file));
            if (ranges.isEmpty() || (ranges.get(0) == full)) {
                // Return full file.
                response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
                response.setHeader("Content-Length", String.valueOf(full.length));
                Range.copy(input, output, length, full.start, full.length);
            } else if (ranges.size() == 1) {
                // Return single part of file.
                final Range r = ranges.get(0);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
                // Copy single part range.
                Range.copy(input, output, length, r.start, r.length);
            } else {
                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + "MULTIPART_BYTERANGES");
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
                // Cast back to ServletOutputStream to get the easy println methods.
                final ServletOutputStream sos = (ServletOutputStream) output;
                // Copy multi part range.
                for (final Range r : ranges) {
                    // Add multipart boundary and header fields for every range.
                    sos.println();
                    sos.println("--" + "MULTIPART_BYTERANGES");
                    sos.println("Content-Type, " + contentType);
                    sos.println("Content-Range, bytes " + r.start + "-" + r.end + "/" + r.total);
                    // Copy single part range of multi part range.
                    Range.copy(input, output, length, r.start, r.length);
                }
                // End with multipart boundary.
                sos.println();
                sos.println("--" + "MULTIPART_BYTERANGES" + "--");
            }
        } catch (final Exception e) {
            // 可能连接被重置
        } finally {
            IOUtils.closeQuietly(input);
        }
    }
}
